Chapter 2談過,每個範疇(scope)就像容器一樣,裡面包含各種的識別子(變數、函式)。
巢狀範疇(nested scopes)在撰寫程式碼時期就已經定義了。
一般認為,在JavaScript中,範疇(scope)是以函式為基礎。
宣告函式的同時,也建立範疇(scope),但並不是只有函式能建立範疇(scope)。
Example
function foo(a) {
var b = 2;
function bar() {
}
var c = 3;
}
在foo( )
中,包含了識別字a、b、c、bar,這些都在foo( )
的範疇之中。
全域範疇中只有一個識別字:foo
。
因為a、b、c、bar是屬於foo( )
的範疇,所以在foo( )
的外面(全域範疇)是無法存取到這些識別字的:
bar(); //失敗
console.log( a, b, c ); //失敗
以上所有的識別字都能在foo( )
內部存取,如此一來,也能運用到JS變數的「動態」本質,接收不同型別的值。
利用函式範疇的特性,將我們的程式碼包在函式裡面,建立起如同「隱藏」的效果,讓外面無法窺探內部的運作。
這種隱藏的手段,主要是來自於軟體設計原理「最小權限原則(Principle of Least Privilege)」。
這個原則指出,我們設計的模組或是API,應該只能透露出最少的細節,並把其他的變數或函式給隱藏起來。
如果我們將所有的變數或函式都宣告於全域範疇之中,那他們都能被其他的內嵌的範疇存取,這會違反最小權限原則,如此一來,我們很有可能會透露應該保有私密性的變數或函式。
function doSomething(a) {
b = a + doSomethingElse( a * 2 );
console.log( b * 3 );
}
function doSomethingElse(a) {
return a - 1;
}
var b;
doSomething( 2 ); // 15
b
與doSomethingElse( )
本應是doSomething( )
內部的工作細節,但它們宣告於全域範疇中,意味著,在doSomething( )
之外也能存取它們,這容易造成非預期的情況發生。正確的作法是應該將那些細節隱藏在doSomething( )
之中。
function doSomething(a) {
function doSomethingElse(a) {
return a - 1;
}
var b;
b = a + doSomethingElse( a * 2 );
console.log( b * 3 );
}
doSomething( 2 ); // 15
如此一來,b
與doSomethingElse( )
完全存在於doSomething( )
的範疇之中,外界無法控制它們,功能也沒有改變,這種設計方式通常被認為是較佳的。
將特定的識別字隱藏在一個範疇之中的另一個好處是,避免同名但不同用途的識別字發生衝突。
function foo() {
function bar(a) {
i = 3; //會變更for迴圈的i
console.log( a + i );
}
for (var i=0; i<10; i++) {
bar( i * 2 ); //無窮迴圈
}
}
foo();
bar( )
內部的i
會意外地影響迴圈的i
,會讓i
永遠為3,導致無窮迴圈。解決方式是在bar( )
內部改成var i = 3;
,讓i
成為bar( )
內的區域變數,而不會影響到for迴圈的另一個i
。
另一個可能發生衝突的情況是,當你的程式載入其他的程式庫,卻沒有正確地隱藏程式庫的私有識別字。
這些程式庫通常會在全域範疇宣告變數或函式,有可能是一個物件。這個物件會被當作命成空間使用,所有要對外的功能都會是物件的屬性,而非宣告在語意範疇中頂層的識別字。
Example
var MyReallyCoolLibrary = {
awesome: "stuff",
doSomething: function() {
// ...
},
doAnotherThing: function() {
// ...
}
};
剛剛已說明我們可以將識別字宣告於函式中,建立其範疇,不讓外面範疇存取。
Example
var a = 2;
function foo() {
var a = 3;
console.log( a ); // 3
}
foo();
console.log( a ); // 2
以上的方式是合理的,但有些問題。首先,我們必須要宣告函式名稱foo
,這本身會汙染到全域範疇。再者,我們還得呼叫foo
函式,裡面的程式碼才會執行。
有個方式可以讓該函式不需要名稱(或許就不會汙染到包含它的範疇),並且能夠自動執行:
var a = 2;
(function foo(){
var a = 3;
console.log( a ); // 3
})();
console.log( a ); // 2
首先我們看到整個函式被包在( )內部,這表示該函式不會被當成函式宣告,而是一個函式運算式(function-expression)。
要分辨宣告與運算式的方法,是看function
的位置。若function
在敘述句的最開頭,那它就是函式宣告;若不是,就是函式運算式。
setTimeout( function(){
console.log("I waited 1 second!");
}, 1000 );
上面的方式被稱為匿名函式運算式(anonymous function expression)。函式運算式可以匿名,但函式宣告不行。
但函式運算式有幾個缺點:
1.函式運算式在堆疊軌跡沒有名字可以顯示,會造成除錯的困難。
2.若函式進行遞迴的話,會得到已被棄用的arguments.callee參考。
3.函式名稱可以增加我們的程式可讀性。
行內涵式運算式(Inline function expressions),可以解決上述問題,是最佳的實務做法:
setTimeout( function timeoutHandler(){ // <--增加函式名稱
console.log( "I waited 1 second!" );
}, 1000 );
var a = 2;
(function foo(){
var a = 3;
console.log( a ); // 3
})();
將函式包在( )之中,並且在尾部加再上()來執行這個運算式。
第1個()將函式變成運算式,第2個()直接執行該函式。
這種方式很普遍,通稱為立即調用函式運算式(IIFE,Immediately Invoked Function Expression)。
IIFE可以使用匿名函式,但若為函式命名,可以減去一些麻煩。
IIFE有另一種表示方式,(function(){ .. }())
,直接把執行的第2個()包在第1個()裡面,以上的2種方式功能都一樣。
IIFE也可以傳入引數:
var a = 2;
(function IIFE( global ){
var a = 3;
console.log( a ); // 3,區域變數a
console.log( global.a ); // 2,全域變數a
})( window );
console.log( a ); // 2
在IIFE( )
內外各宣告變數a
,並傳入一個window物件,如此一來,我們可以區分全域參考與區域參考的差異。
let與varㄧ樣都是宣告變數的關鍵字,不同的是,以let宣告的變數,其範疇是以區塊{}
作為基準。
var foo = true;
if (foo) {
let bar = foo * 2;
bar = something( bar );
console.log( bar );
}
console.log( bar ); // 擲出ReferenceError
因為以let宣告的變數bar
,它的範疇限制在if敘述句的大括號{ }中,意味著在此範疇外的環境無法存取bar
。
for (let i=0; i<10; i++) {
console.log( i );
}
console.log( i ); // ReferenceError
在for迴圈使用let宣告,let不只會將i繫結到for主體,還會重新繫結迴圈每跑一次的值,會將前一個i值再重新指定給i。
const也會建立以區塊{}為範疇的變數,比較特別的是,宣告時就必須要一併給值,其值是固定不變的,之後任何改變值的行為都會擲出錯誤。
此為You Don't Know JS系列的筆記。